/**
 * 建模持久化工具
 */
import tnx from '@/tnx.js';
import MavenProjectUtil from '@/logic/maven/util/project.js';
import ModelingApp from '../entity/ModelingApp.js';
import GenericType from '../entity/GenericType.js';
import EnumClass from '../entity/EnumClass.js';
import DependencyLib from '../entity/DependencyLib.js';
import DependencyClass from '../entity/DependencyClass.js';
import ModelingClass from '../entity/ModelingClass.js';
import DiscriminatorColumn from '../entity/DiscriminatorColumn.js';
import TypeKind from '../enums/TypeKind.js';
import DependencyLibType from '../enums/DependencyLibType.js';
import InheritanceStrategy from '../enums/InheritanceStrategy.js';
import PhysicalIndexType from '../enums/PhysicalIndexType.js';
import {ENUM_CLASS_NAME} from './dependency-lib-jdk.js';
import PhysicalTypes, {getDefaultPhysicalType} from './database-physical-types.js';

export default {
    getAppPath(mavenProject) {
        return tnx.util.file.getParentDir(mavenProject.path) + mavenProject.artifactId + '.tnxml';
    },
    saveApp(app, path) {
        path = path || app.path;
        return new Promise((resolve, reject) => {
            let xml = '<?xml version="1.0" encoding="UTF-8"?>\n' +
                '<app type="server">\n' +
                '    <name>' + (app.name || '') + '</name>\n' +
                '    <caption>' + (app.caption || '') + '</caption>\n';
            if (app.databaseVendor) {
                xml += '    <databaseVendor>' + app.databaseVendor + '</databaseVendor>\n';
            }
            if (app.tableNameRule) {
                let tableNameRuleXml = tnx.util.xml.toFormattedXml(app.tableNameRule, 8, true);
                if (tableNameRuleXml.trim()) {
                    xml += '    <tableNameRule>\n' + tableNameRuleXml + '    </tableNameRule>\n';
                }
            }

            const ignoredKeys = ['name', 'final', 'modified'];
            const classToXml = function (clazz, indent) {
                indent = indent || 0;
                let indentPrefix = ' '.repeat(indent);
                let xml = indentPrefix + '<class>\n';
                if (clazz instanceof EnumClass) {
                    xml += tnx.util.xml.toFormattedXml(clazz, indent + 4, true, key => {
                        if (ignoredKeys.includes(key)) {
                            return undefined;
                        }
                        if (key === 'constants') {
                            return 'constant';
                        }
                        if (key === 'subCaptions') {
                            return 'subCaption';
                        }
                        // 子级字段名需去掉上级字段名的部分
                        key = key.substring(key.lastIndexOf('.') + 1);
                        if (key === 'scenes') {
                            key = 'scene';
                        }
                        return key;
                    });
                } else {
                    xml += tnx.util.xml.toFormattedXml(clazz, indent + 4, true, key => {
                        if (ignoredKeys.includes(key)) {
                            return undefined;
                        }
                        if (['property.genericType.id', 'property.genericType.kind', 'declaredGenericType.id',
                            'declaredGenericType.kind', 'instantiatedGenericType.id', 'instantiatedGenericType.kind']
                            .includes(key)) {
                            return undefined;
                        }
                        // 子级字段名需去掉上级字段名的部分
                        key = key.substring(key.lastIndexOf('.') + 1);
                        if (key === 'properties') {
                            return 'property';
                        }
                        if (key === 'keywordMappingIds') {
                            return 'keywordMappingId';
                        }
                        if (key === 'indexes') {
                            return 'index';
                        }
                        if (key === 'mappingIds') {
                            return 'mappingId';
                        }
                        if (key.firstToUpperCase().endsWith('GenericTypes')) {
                            return key.substring(0, key.length - 1);
                        }
                        if (['final', 'typeName', 'refType'].includes(key)) {
                            return undefined;
                        }
                        return key;
                    });
                }
                xml += indentPrefix + '</class>\n';
                return xml;
            }

            if (app.classes) {
                for (let clazz of app.classes) {
                    xml += classToXml(clazz, 4);
                }
            }
            if (app.dependencies?.length) {
                let dependenciesXml = '';
                for (let dependency of app.dependencies) {
                    let classesXml = '';
                    for (let dependencyClass of dependency.classes) {
                        if (dependencyClass.referencedNum) { // 只持久化有引用的类
                            let origin = dependencyClass.origin.clone();
                            classesXml += classToXml(origin, 8);
                        }
                    }
                    if (classesXml.trim()) {
                        dependenciesXml += '    <dependency>\n';
                        dependenciesXml += tnx.util.xml.toFormattedXml(dependency, 8, true, key => {
                            if (['favoriteClassNames', 'classes'].includes(key)) {
                                return undefined;
                            }
                            return key;
                        }, (value, key) => {
                            if (key === 'modelingPath') {
                                return tnx.util.file.toRelativePath(path, true, dependency.modelingPath);
                            }
                            return value;
                        });
                        dependenciesXml += classesXml;
                        dependenciesXml += '    </dependency>\n';
                    }
                }
                if (dependenciesXml.trim()) {
                    xml += dependenciesXml;
                }
            }
            xml += '</app>\n';
            tnx.util.file.write(path, xml).then(resolve).catch(reject);
        });
    },
    syncSubClasses(project, superClass, predicate) {
        for (let app of project.apps) {
            let b = app.syncSubClasses(superClass);
            if (typeof predicate === 'function') {
                b = predicate(app) || b;
            }
            if (b) {
                this.saveApp(app).then(() => {
                });
            }
        }
    },
    loadRelativeMavenProject(modelingPath) {
        return new Promise((resolve, reject) => {
            let pomPath = tnx.util.file.getParentDir(modelingPath) + 'pom.xml';
            MavenProjectUtil.loadProject(pomPath).then(resolve).catch((error, exists) => {
                if (exists === false) {
                    tnx.error('不存在对应的Maven工程pom.xml文件：' + pomPath);
                } else {
                    reject(error);
                }
            });
        });
    },
    readDatabaseVendor(modelingPath) {
        return new Promise((resolve, reject) => {
            this.loadRelativeMavenProject(modelingPath).then(mavenProject => {
                MavenProjectUtil.readDatabaseVendor(mavenProject).then(resolve).catch(reject);
            }).catch(reject);
        });
    },
    loadApp(path) {
        return new Promise((resolve, reject) => {
            tnx.util.file.read(path, true).then((xml, meta) => {
                // noinspection VueMissingComponentImportInspection
                let index1 = xml.indexOf('<app type="server">');
                if (index1 >= 0) {
                    let index2 = xml.indexOf('</app>');
                    if (index2 > index1) {
                        xml = xml.substring(index1, index2 + '</app>'.length);
                        let app = this.parseApp(path, xml);
                        if (app) {
                            app.saveTime = meta.modifiedTime;
                            this.loadModelDependencyLibs(app).then(() => {
                                resolve(app);
                            }).catch(reject);
                            return;
                        }
                    }
                }
                reject(path + ' 不是有效的建模应用文件');
            }).catch(error => {
                let exists = true;
                if (Array.isArray(error)) {
                    exists = error[1];
                    error = error[0];
                }
                if (exists) {
                    reject(error);
                } else {
                    console.warn(error);
                    resolve();
                }
            });
        });
    },
    parseApp(path, xml) {
        let model = tnx.util.xml.parse(xml);
        if (model) {
            model = model.app;
            if (model) {
                let classes = [];
                if (model.class) {
                    let modelClasses = Array.isArray(model.class) ? model.class : [model.class];
                    for (let clazz of modelClasses) {
                        classes.push(this._transformClass(clazz));
                    }
                }
                let dependencies = [];
                if (model.dependency) {
                    let modelDependencies = Array.isArray(model.dependency) ? model.dependency : [model.dependency];
                    for (let dependency of modelDependencies) {
                        dependency.type = DependencyLibType.valueOf(dependency.type);
                        dependency.type = dependency.type || DependencyLibType.MODELING;
                        if (dependency.modelingPath) {
                            dependency.modelingPath = tnx.util.file.toAbsolutePath(path, true, dependency.modelingPath);
                        }
                        if (dependency.modelingSyncTime) {
                            dependency.modelingSyncTime = new Date(dependency.modelingSyncTime);
                        }
                        if (dependency.class) {
                            let modelDependencyClasses = Array.isArray(dependency.class)
                                ? dependency.class : [dependency.class];
                            dependency.classes = [];
                            for (let modelDependencyClass of modelDependencyClasses) {
                                modelDependencyClass = this._transformClass(modelDependencyClass);
                                let origin = ModelingApp.ofClass(modelDependencyClass);
                                if (origin) {
                                    dependency.classes.push(new DependencyClass(origin));
                                }
                            }
                            dependencies.push(dependency);
                        }
                    }
                }

                let app = ModelingApp.of({
                    path: path,
                    name: model.name,
                    caption: model.caption,
                    tableNameRule: model.tableNameRule,
                    classes: classes,
                    dependencies: dependencies,
                });
                app.syncReferences();
                return app;
            }
        }
        return undefined;
    },
    _transformClass(clazz) {
        clazz.kind = TypeKind.validate(clazz.kind);
        if (clazz.saveTime) {
            clazz.saveTime = new Date(clazz.saveTime);
        }
        clazz.modified = false;

        if (TypeKind.ENUM.equals(clazz.kind)) {
            clazz.simple = clazz.simple === 'true';
            if (clazz.constant) {
                if (Array.isArray(clazz.constant)) {
                    clazz.constants = clazz.constant;
                } else {
                    clazz.constants = [clazz.constant];
                }
                for (let constant of clazz.constants) {
                    if (Array.isArray(constant.scene)) {
                        constant.scenes = constant.scene;
                    } else {
                        constant.scenes = [constant.scene];
                    }
                }
                delete clazz.constant;
            }
            if (clazz.subCaption) {
                if (Array.isArray(clazz.subCaption)) {
                    clazz.subCaptions = clazz.subCaption;
                } else {
                    clazz.subCaptions = [clazz.subCaption];
                }
                delete clazz.subCaption;
            }
        } else {
            if (clazz.abstractClass) {
                clazz.abstractClass = clazz.abstractClass === 'true';
            }
            if (clazz.inheritanceStrategy) {
                clazz.inheritanceStrategy = InheritanceStrategy.validate(clazz.inheritanceStrategy);
            }
            if (clazz.discriminatorColumn) {
                clazz.discriminatorColumn = DiscriminatorColumn.of(clazz.discriminatorColumn);
            }
            if (clazz.declaredGenericType) {
                if (Array.isArray(clazz.declaredGenericType)) {
                    clazz.declaredGenericTypes = clazz.declaredGenericType;
                } else {
                    clazz.declaredGenericTypes = [clazz.declaredGenericType];
                }
                delete clazz.declaredGenericType;
                for (let i = 0; i < clazz.declaredGenericTypes.length; i++) {
                    clazz.declaredGenericTypes[i] = GenericType.of(clazz.declaredGenericTypes[i]);
                }
            }
            if (clazz.instantiatedGenericType) {
                if (Array.isArray(clazz.instantiatedGenericType)) {
                    clazz.instantiatedGenericTypes = clazz.instantiatedGenericType;
                } else {
                    clazz.instantiatedGenericTypes = [clazz.instantiatedGenericType];
                }
                delete clazz.instantiatedGenericType;
                for (let i = 0; i < clazz.instantiatedGenericTypes.length; i++) {
                    clazz.instantiatedGenericTypes[i] = GenericType.of(clazz.instantiatedGenericTypes[i]);
                }
            }
            if (clazz.property) {
                if (Array.isArray(clazz.property)) {
                    clazz.properties = clazz.property;
                } else {
                    clazz.properties = [clazz.property];
                }
                delete clazz.property;

                const transformProperty = property => {
                    property.arrayed = property.arrayed === 'true';
                    property.derived = property.derived === 'true';
                    if (property.genericType) {
                        if (Array.isArray(property.genericType)) {
                            property.genericTypes = property.genericType;
                        } else {
                            property.genericTypes = [property.genericType];
                        }
                        delete property.genericType;
                        for (let i = 0; i < property.genericTypes.length; i++) {
                            property.genericTypes[i] = GenericType.of(property.genericTypes[i]);
                        }
                    }
                }

                for (let property of clazz.properties) {
                    transformProperty(property);
                    if (clazz.kind.supports('domain')) {
                        if (typeof property.caption !== 'string') {
                            property.caption = '';
                        }
                        property.manualInput = property.manualInput === 'true';
                        property.regional = property.regional === 'true';
                    }
                    if (clazz.kind.supports('entity')) {
                        property.primary = property.primary === 'true';
                        property.belongs = property.belongs === 'true';
                        if (property.mapping) {
                            property.mapping.nullable = property.mapping.nullable === 'true';
                            property.mapping.length = property.mapping.length ? parseInt(property.mapping.length) : 0;
                            property.mapping.scale = property.mapping.scale ? parseInt(property.mapping.scale) : 0;
                            if (property.mapping.querying) {
                                property.mapping.querying.fuzzy = property.mapping.querying.fuzzy === 'true';
                                transformProperty(property.mapping.querying);
                            }
                            if (property.mapping.command) {
                                transformProperty(property.mapping.command);
                            }
                        }
                        if (property.embedded) {
                            if (!Array.isArray(property.embedded)) {
                                property.embedded = [property.embedded];
                            }
                            for (let embeddedMapping of property.embedded) {
                                embeddedMapping.nullable = embeddedMapping.nullable === 'true';
                                embeddedMapping.length = embeddedMapping.length ? parseInt(embeddedMapping.length) : 0;
                                embeddedMapping.scale = embeddedMapping.scale ? parseInt(embeddedMapping.scale) : 0;
                                if (embeddedMapping.querying) {
                                    embeddedMapping.querying.fuzzy = embeddedMapping.querying.fuzzy === 'true';
                                    transformProperty(embeddedMapping.querying);
                                }
                                if (embeddedMapping.command) {
                                    transformProperty(embeddedMapping.command);
                                }
                            }
                        }
                    }
                }
            }
            if (clazz.keywordMappingId) {
                if (Array.isArray(clazz.keywordMappingId)) {
                    clazz.keywordMappingIds = clazz.keywordMappingId;
                } else {
                    clazz.keywordMappingIds = [clazz.keywordMappingId];
                }
                delete clazz.keywordMappingId;
            }
            if (clazz.index) {
                if (Array.isArray(clazz.index)) {
                    clazz.indexes = clazz.index;
                } else {
                    clazz.indexes = [clazz.index];
                }
                delete clazz.index;
                for (let index of clazz.indexes) {
                    index.type = PhysicalIndexType.validate(index.type);
                    if (Array.isArray(index.mappingId)) {
                        index.mappingIds = index.mappingId;
                    } else {
                        index.mappingIds = [index.mappingId];
                    }
                    delete index.mappingId;
                }
            }
        }

        return clazz;
    },
    loadModelDependencyLibs(app) {
        return new Promise((resolve, reject) => {
            this.loadRelativeMavenProject(app.path).then(mavenProject => {
                MavenProjectUtil.readDependencyModelModuleDir(mavenProject).then(dependencyModelModuleDir => {
                    if (dependencyModelModuleDir) {
                        let dependencyModelPomPath = tnx.util.file.joinPath(dependencyModelModuleDir, 'pom.xml');
                        MavenProjectUtil.loadProject(dependencyModelPomPath, false)
                            .then(dependencyModelMavenProject => {
                                let dependencyAppDir = dependencyModelModuleDir.substring(0,
                                    dependencyModelModuleDir.lastIndexOf(tnx.util.file.separator));
                                let dependencyAppName = dependencyAppDir.substring(
                                    dependencyAppDir.lastIndexOf(tnx.util.file.separator) + 1);
                                let dependencyModelingPath = tnx.util.file.joinPath(dependencyAppDir,
                                    dependencyAppName + '.tnxml');
                                this.loadApp(dependencyModelingPath).then(dependencyModelingApp => {
                                    if (dependencyModelingApp) {
                                        // 依赖的应用生成建模依赖库
                                        let appLib = new DependencyLib(DependencyLibType.MODELING);
                                        appLib.groupId = dependencyModelMavenProject.groupId;
                                        appLib.artifactId = dependencyModelMavenProject.artifactId;
                                        appLib.version = dependencyModelMavenProject.version;
                                        appLib.modelingPath = dependencyModelingPath;
                                        for (let modelingClass of dependencyModelingApp.classes) {
                                            appLib.pushClass(modelingClass);
                                        }
                                        let libs = [appLib];
                                        // 依赖建模应用的依赖库也作为当前应用的依赖库
                                        libs.push(...dependencyModelingApp.dependencies);
                                        for (let newLib of libs) {
                                            let oldLib = app.dependencies.find(l => l.matches(newLib));
                                            if (oldLib) {
                                                // 建模依赖库，加入原库中没有的类，已有的模型类记录保存时间，以便于判断是否需要同步更新
                                                if (DependencyLibType.MODELING.equals(newLib.type)) {
                                                    for (let newClass of newLib.classes) {
                                                        let newOrigin = newClass.origin;
                                                        // 用id比较匹配，以避免类名变更导致的差异
                                                        let oldDependencyClass = oldLib.classes.find(
                                                            c => c.origin.id === newOrigin.id);
                                                        if (oldDependencyClass) {
                                                            // 注意：此举只能记录加载时的模型类保存时间，在此之后如果模型类改动，
                                                            // 不会同步记录，因而该字段目前只能用于进行是否需要同步更新的判断
                                                            if (newOrigin instanceof ModelingClass) {
                                                                oldDependencyClass.lastSaveTime = newOrigin.saveTime;
                                                            } else {
                                                                oldDependencyClass.lastSaveTime = null;
                                                            }
                                                        } else {
                                                            oldLib.pushClass(newOrigin);
                                                        }
                                                    }
                                                }
                                            } else {
                                                newLib.modelingSyncTime = new Date();
                                                app.dependencies.push(newLib);
                                            }
                                        }
                                        app.sortDependencies();
                                        resolve();
                                    } else {
                                        resolve();
                                    }
                                }).catch(reject);
                            })
                            .catch(reject);
                    } else {
                        resolve([]);
                    }
                }).catch(reject);
            }).catch(reject);
        });
    },
    getAvailablePhysicalTypes(app) {
        let database = app.databaseVendor || Object.keys(PhysicalTypes)[0];
        return PhysicalTypes[database];
    },
    getFinalLogicType(app, logicType) {
        let clazz = app.findClass(logicType, true);
        if (clazz) {
            let refKind = clazz.getKind();
            if (TypeKind.ENUM.equals(refKind)) {
                return ENUM_CLASS_NAME;
            } else if (refKind.supports('association')) {
                return app.getPrimaryLogicTypeIds(clazz)[0];
            }
        }
        return logicType;
    },
    getDefaultPhysicalType(app, logicType, arrayed) {
        let physicalTypes = this.getAvailablePhysicalTypes(app);
        let finalLogicType = this.getFinalLogicType(app, logicType);
        return getDefaultPhysicalType(physicalTypes, finalLogicType, arrayed);
    },
    findPhysicalType(app, physicalTypeName) {
        let physicalTypes = this.getAvailablePhysicalTypes(app);
        return physicalTypes.find(pt => pt.name === physicalTypeName);
    },
}
